package fctuc.dei.myTwitter.com;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;

/**
 * @author <a:mailto="becarv@dei.uc.pt" />Bruno de Carvalho</a>
 */
public class Serializer {

    /**
     * Serializes any instance of a class that implements Serializable interface (watch out for non-serializable
     * properties such as Maps and the likes of it... use "transient" modifier in those fields!!!)
     *
     * @param object Object to be serialized.
     *
     * @return ByteBuffer containing the serialized object, preceded with 4 bytes indicating its length.
     *
     * @throws IOException Rethrows IOExceptions caught while performing ByteArrayOutputStream/ObjectOutputStream
     *                     operations.
     */
    public static ByteBuffer serializeObject(Object object) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(object);
        objectOutputStream.flush();
        objectOutputStream.close();
        outputStream.close();

        // this is the object in byte[] form
        byte[] serializedObject = outputStream.toByteArray();

        // allocate serializedObject.length + 4 bytes (size of an integer)
        ByteBuffer buffer = ByteBuffer.allocate(serializedObject.length + 4);

        // write the size of the object to be Serialized
        buffer.putInt(serializedObject.length);

        // write the object
        buffer.put(serializedObject);
        buffer.flip();

        return buffer;
    }

    public static Object deserializeObject(ByteBuffer buffer) throws IOException, ClassNotFoundException {
        if (buffer.remaining() <= 4) {
            System.err.println("Serializable.deserializeObject(): Buffer size is " + buffer.remaining() +
                               ": at least 4 + N (more than 4) bytes are necessary to deserialize an object.");
            throw new BufferUnderflowException();
        }

        // will throw BufferUndeflowException if ByteBuffer contains less than 4 bytes...
        int objectLength = buffer.getInt();
        if (objectLength < 0) {
            // do keep in mind that there is very very slim chance that the invalid Integer read from the start of the
            // buffer will actually match the remaining data... however, a deserialization exception will occur later.
            throw new IllegalArgumentException("Invalid serialized object size: " +
                                               "Integer at start of buffer contains invalid data.");
        }

        return deserializeObject(buffer, objectLength);
    }

    public static Object deserializeObject(ByteBuffer buffer, int objectLength)
            throws IOException, ClassNotFoundException {
        if (buffer.remaining() <= 0) {
            System.err.println("Serializable.deserializeObject(): Buffer size is " + buffer.remaining() +
                               ": at least N (> 0) bytes are necessary to deserialize an object.");
            throw new BufferUnderflowException();
        }

        if (buffer.remaining() < objectLength) {
            System.err.println("Serializable.deserializeObject(): Buffer indicates that objectLength is " +
                               objectLength + " but there are only " + buffer.remaining() + " bytes in the buffer.");
            throw new BufferUnderflowException();
        }

        byte[] serializedObject = new byte[objectLength];
        buffer.get(serializedObject);
        ByteArrayInputStream inputStream = new ByteArrayInputStream(serializedObject);
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        Object object = objectInputStream.readObject();
        inputStream.close();
        objectInputStream.close();

        return object;
    }
}
